fix(mcp): emit search results once instead of content + structuredContent - #14175
Open
swissmike-zh wants to merge 1 commit into
Open
fix(mcp): emit search results once instead of content + structuredContent#14175swissmike-zh wants to merge 1 commit into
swissmike-zh wants to merge 1 commit into
Conversation
…tent search_indexed_documents is annotated -> dict[str, Any], so FastMCP derives an output schema and sends the payload twice: once as structuredContent and again as serialised JSON in a text block. For a search tool the payload is the retrieved document content, so the duplicate scales with everything retrieved - measured at ~50% of the tool result on a realistic payload. The derived schema comes from dict[str, Any] and conveys no structure, so clients gain nothing from the structured copy. Returning an explicit ToolResult emits the payload once; the content text is byte-identical, so consumers parsing the text path are unaffected. search_web and open_urls are left as-is - they dual-emit too, but their payloads are small enough not to matter.
Contributor
|
PR author is not in the allowed authors list. |
Contributor
There was a problem hiding this comment.
1 issue found across 1 file
Confidence score: 1/5
backend/onyx/mcp_server/tools/search.pyimportsToolResultfrom a path absent in the pinned FastMCP 3.2.0, causingModuleNotFoundErrorduring import and preventing the MCP server from starting — update the import to the compatiblefastmcp.tools.bas...path.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/onyx/mcp_server/tools/search.py">
<violation number="1" location="backend/onyx/mcp_server/tools/search.py:10">
P0: With the repository's pinned FastMCP 3.2.0, this import path does not exist, so importing `search.py` raises `ModuleNotFoundError` and prevents the MCP server from starting. Import `ToolResult` from `fastmcp.tools.base` instead.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| import httpx | ||
| import pydantic_core | ||
| from fastmcp.server.auth.auth import AccessToken | ||
| from fastmcp.tools.tool import ToolResult |
Contributor
There was a problem hiding this comment.
P0: With the repository's pinned FastMCP 3.2.0, this import path does not exist, so importing search.py raises ModuleNotFoundError and prevents the MCP server from starting. Import ToolResult from fastmcp.tools.base instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/onyx/mcp_server/tools/search.py, line 10:
<comment>With the repository's pinned FastMCP 3.2.0, this import path does not exist, so importing `search.py` raises `ModuleNotFoundError` and prevents the MCP server from starting. Import `ToolResult` from `fastmcp.tools.base` instead.</comment>
<file context>
@@ -5,7 +5,9 @@
import httpx
+import pydantic_core
from fastmcp.server.auth.auth import AccessToken
+from fastmcp.tools.tool import ToolResult
from pydantic import BaseModel, TypeAdapter, ValidationError
</file context>
Suggested change
| from fastmcp.tools.tool import ToolResult | |
| from fastmcp.tools.base import ToolResult |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14171
Problem
search_indexed_documentsis annotated-> dict[str, Any], so FastMCP derives an output schema from the annotation and sends the payload twice — once asstructuredContent, and again as serialised JSON in a text content block. Every search result crosses the wire in duplicate.For most tools that is cheap. For a search tool the payload is the retrieved document content, so the duplicate scales with everything retrieved.
Measurement
On
fastmcp==3.2.0(the pinned version), with a single-document payload:-> dict[str, Any])-> ToolResult)In a self-hosted deployment with an agent client that forwards tool results to the model without capping, this was worth roughly 2.4k input tokens per search — about half the tool-result injection. Full standalone reproduction in #14171.
Why this is safe
pydantic_core.to_jsonof the same dict — so anything parsing the text path sees no change.dict[str, Any], which describes no structure, so clients gain nothing from the structured copy for this tool.search_webandopen_urlsare deliberately left as-is. They dual-emit too, but their payloads are small enough not to matter, and_error_payloadstays a plain dict sinceopen_urlsshares it.Note
output_schema=Noneon the decorator does not fix this — it drops the declaredoutputSchemafromtools/listbut the result still carries both representations. Verified on 3.2.0. An explicitToolResultis what suppresses the duplicate.Caveat
Dual emission is the spec-compliant default when a tool declares an output schema, so a client genuinely consuming the structured path for this tool would be affected. If you would rather have this behind a flag than as a change of default, I am happy to rework it — or to close this in favour of an approach you prefer.
Checks
ruff formatclean (0.16.0, per.pre-commit-config.yaml)ruff checkreports no new findings — identical counts before and afterSummary by cubic
Emit
search_indexed_documentsresults once by returning aToolResultinstead of a bare dict, removing the duplicatestructuredContent. This halves the wire size on typical payloads and reduces token usage without changing the content text.fastmcpderived a schema fromdict[str, Any]and sent bothstructuredContentand text; now only text content is sent.search_webandopen_urlsare unchanged.structuredContentforsearch_indexed_documents, switch to parsing thecontenttext.Written for commit 2ae57d2. Summary will update on new commits.